Short Explanation
C++'s type system simply doesn't support it. In any case int** is not the correct type for a pointer to a dynamically-allocated 2D array. What you want is int (*)[N]: Pointer to an array of length N. Types only exist at compile-time though, so N must be a compile-time constant for that type to be valid.
In theory, the language could support dynamically-allocated arrays of arrays of dynamic bound, but such a thing can't be done with simple pointers, since data about the size of each dynamic bound has to be stored somewhere so that the array can be traversed. Since C++ has classes, and classes can be used to handle that job, there has never been a need to add language-level support.
Longer explanation
First, consider what a 1D array of ints looks like in memory: just a contiguous block of ints:
┌───┬───┬─────┬───┐
│ 0 │ 1 │ ... │ N │
└───┴───┴─────┴───┘
Now, an int* can point to any of those elements:
int* array1d
│
┌─▼─┬───┬─────┬───┐
│ 0 │ 1 │ ... │ N │
└───┴───┴─────┴───┘
And since the compiler knows how big an int is, you can navigate from element to element simply by offsetting the pointer by that size. That is, to get from array1d to array1d[N], you simply shift the pointer N * sizeof(int) bytes.
Now, consider how a 2D array is laid out in memory. It's a continuous block of 1D array elements, each of which is a contiguous block of some number of ints:
┌───────────────────┬───────────────────┬───────────────────┬───────────────────┐
│┌───┬───┬─────┬───┐│┌───┬───┬─────┬───┐│┌─────────────────┐│┌───┬───┬─────┬───┐│
││ 0 │ 1 │ ... │ N │││ 0 │ 1 │ ... │ N │││ ... │││ 0 │ 1 │ ... │ N ││
│└───┴───┴─────┴───┘│└───┴───┴─────┴───┘│└─────────────────┘│└───┴───┴─────┴───┘│
└───────────────────┴───────────────────┴───────────────────┴───────────────────┘
You've declared array2d to be a pointer to a pointer to an int, so it needs to point to an int*, but there is no int* here for it to point to, so int** must be the wrong type.
Lets use something like int (*)[]: pointer to an array of unknown bounds of int. That could conceivably point to the first element of our array of arrays of int:
int (*array2d)[]
│
┌──▼────────────────┬───────────────────┬───────────────────┬───────────────────┐
│┌───┬───┬─────┬───┐│┌───┬───┬─────┬───┐│┌─────────────────┐│┌───┬───┬─────┬───┐│
││ 0 │ 1 │ ... │ N │││ 0 │ 1 │ ... │ N │││ ... │││ 0 │ 1 │ ... │ N ││
│└───┴───┴─────┴───┘│└───┴───┴─────┴───┘│└─────────────────┘│└───┴───┴─────┴───┘│
└───────────────────┴───────────────────┴───────────────────┴───────────────────┘
But there's a problem with this setup. The compiler doesn't know how big the array pointed to by array2d is. Thus, when you ask for array2d[1] it doesn't know how far to offset the pointer to find the next element in the array. It can't shift the pointer sizeof(int[]) bytes, because sizeof(int[]) isn't known.
Information about the size of each array element doesn't exist until runtime, but the compiler needs to generate machine code, at compile time, to do the correct offset.
And so, only arrays of arrays of constant bound can be allocated dynamically via the new keyword. It then returns an int (*)[N] (or int (*)[N][M], etc). Since N is a compile-time constant, the compiler knows how far to offset the pointer to find each subsequent element in the array.
array2dis not a 2D array (although it can be used to represent one) - but rather a pointer to a pointer toint.