The parameter of the function print has the type int *.
print(int *p)
So dereferencing the pointer in the expression (*p)[i] you will get a scalar object of the type int. You may not apply the subscript operator to scalar objects of the type int.
On the other hand, in this call
print(arr);
the argument that has the type int[3][3] is converted to pointer to its first element. Elements of the array have the type int[3]. So the type of the expression after the implicit conversion of the array to pointer to its first element is int ( * )[3].
And the error message points to this problem
Main.cpp:16:4: error: no matching function for call to 'print'
print(arr);
   ^~~~~
because the compiler is unable to find a function with the name print that accepts an argument of the type int ( * )[3].
Thus the parameter of the function print should be declared like
print( int p[][3] )
or
print( int ( *p )[3] )
And as the array is not changed in the function it should be declared with the qualifier const.
The function definition in this case will look like (if you want to use pointers)
void print( const int p[][3] )
{
    for( const int ( *row )[3] = p; row != p + 3; ++row ) 
    {
        for ( const int *col = *row; col != *row + 3; ++col )
        {
            std::cout << *col << ' ';
        }
        std::cout << '\n';
    }
}
Here is a demonstrative program.
#include <iostream>
void print( const int p[][3] )
{
    for( const int ( *row )[3] = p; row != p + 3; ++row ) 
    {
        for ( const int *col = *row; col != *row + 3; ++col )
        {
            std::cout << *col << ' ';
        }
        std::cout << '\n';
    }
}
int main() 
{
    const size_t N = 3;
    int arr[N][N] = 
    {
        { 1, 2, 3 } ,
        { 4, 5, 6 } ,
        { 7, 8, 9 }
    };
    print( arr );
    return 0;
}
Its output is
1 2 3 
4 5 6 
7 8 9
However this approach has a serious drawback. The function uses magic number 3. 
It is better to rewrite the function at least like
#include <iostream>
const size_t N = 3;
void print( const int p[][N], size_t rows )
{
    for( const int ( *row )[N] = p; row != p + rows; ++row ) 
    {
        for ( const int *col = *row; col != *row + N; ++col )
        {
            std::cout << *col << ' ';
        }
        std::cout << '\n';
    }
}
int main() 
{
    int arr[][N] = 
    {
        { 1, 2, 3 } ,
        { 4, 5, 6 } ,
        { 7, 8, 9 }
    };
    print( arr, sizeof( arr ) / sizeof( *arr ) );
    return 0;
}
Also you could add one more parameter with a default argument. For example
std::ostream & print( const int p[][N], size_t rows, std::ostream &os = std::cout )
{
    for( const int ( *row )[N] = p; row != p + rows; ++row ) 
    {
        for ( const int *col = *row; col != *row + N; ++col )
        {
            os << *col << ' ';
        }
        os << '\n';
    }
    return os;
}
For example
#include <iostream>
const size_t N = 3;
std::ostream & print( const int p[][N], size_t rows, std::ostream &os = std::cout )
{
    for( const int ( *row )[N] = p; row != p + rows; ++row ) 
    {
        for ( const int *col = *row; col != *row + N; ++col )
        {
            os << *col << ' ';
        }
        os << '\n';
    }
    return os;
}
int main() 
{
    int arr[][N] = 
    {
        { 1, 2, 3 } ,
        { 4, 5, 6 } ,
        { 7, 8, 9 }
    };
    print( arr, sizeof( arr ) / sizeof( *arr ) ) << '\n';
    return 0;
}
And at last you could write a template function.
#include <iostream>
template <typename T, size_t N>
std::ostream & print( const T ( &p )[N][N], std::ostream &os = std::cout )
{
    for( const int ( *row )[N] = p; row != p + N; ++row ) 
    {
        for ( const int *col = *row; col != *row + N; ++col )
        {
            os << *col << ' ';
        }
        os << '\n';
    }
    return os;
}
int main() 
{
    const size_t N = 3;
    int arr[][N] = 
    {
        { 1, 2, 3 } ,
        { 4, 5, 6 } ,
        { 7, 8, 9 }
    };
    print( arr ) << '\n';
    return 0;
}