First, let's look at why you can't assign int arr[2][3] to int **. To make it easier to visualise, we'll initialise your array with a sequence, and consider what it looks like in memory:
int arr[2][3] = {{1,2,3},{4,5,6}};
In memory, the array data is stored as a single block, just like a regular, 1D array:
arr: [ 1, 2, 3, 4, 5, 6 ]
The variable arr contains the address of the start of this block, and from its type (int[2][3]) the compiler knows to interpret an index like arr[1][0] as meaning "take the value that is at position (1*2 + 0) in the array".
However for a pointer-to-pointer (int**), it is expected that the pointer-to-pointer contains either a single memory address or an array of memory addresses, and this/these adress(es) point to (an)other single int value or array of ints. Let's say we copied the array arr into int **ptrptr. In memory, it would look like this:
ptrptr:     [0x203F0B20, 0x203F17D4]
0x203F0B20: [ 1, 2, 3 ]
0x203F17D4: [ 4, 5, 6 ]
So in addition to the actual int data, an extra pointer must be stored for each row of the array. Rather than converting the two indexes into a single array lookup, access must be performed by making a first array lookup ("take the second value in ptrptr to get an int*"), then nother array lookup ("take the first value in the array at the address held by the previously obtained int*").
Here's a program that illustrates this:
#include <iostream>
int main()
{
    int arr[2][3] = {{1,2,3},{4,5,6}};
    std::cout << "Memory addresses for int arr[2][3]:" << std::endl;
    for (int i=0; i<2; i++)
    {
        for (int j=0; j<3; j++)
        {
            std::cout << reinterpret_cast<void*>(&arr[i][j]) << ": " << arr[i][j] << std::endl;
        }
    }
    std::cout << std::endl << "Memory addresses for int **ptrptr:" << std::endl;
    int **ptrptr = new int*[2];
    for (int i=0; i<2; i++)
    {
        ptrptr[i] = new int[3];
        for (int j=0; j<3; j++)
        {
            ptrptr[i][j] = arr[i][j];
            std::cout << reinterpret_cast<void*>(&ptrptr[i][j]) << ": " << ptrptr[i][j] << std::endl;
        }
    }
    // Cleanup
    for (int i=0; i<2; i++)
    {
        delete[] ptrptr[i];
        ptrptr[i] = nullptr;
    }
    delete[] ptrptr;
    ptrptr = nullptr;
    return 0;
}
Output:
Memory addresses for int arr[2][3]:
0x7ecd3ccc0260: 1
0x7ecd3ccc0264: 2
0x7ecd3ccc0268: 3
0x7ecd3ccc026c: 4
0x7ecd3ccc0270: 5
0x7ecd3ccc0274: 6
Memory addresses for int **ptrptr:
0x38a1a70: 1
0x38a1a74: 2
0x38a1a78: 3
0x38a1a90: 4
0x38a1a94: 5
0x38a1a98: 6
Notice how the memory addresses always increase by 4 bytes for arr, but for ptrptr there is a jump of 24 bytes between values 3 and 4.
A simple assignment can't create the pointer-to-pointer structure needed for type int **, which is why the loops were necessary in the above program. The best it can do is to decay the int[2][3] type into a pointer to a row of that array, i.e. int (*)[3]. That's what your auto ptr = arr; ends up as.