You will need to use some form of heap-storage for a user-specified "2D-array" (e.g. a vector<vector<T>>). This is necessary since arrays have their sizes fixed at compile-time.
Because the value won't be known until runtime, there really isn't an alternative available. That said, there are two different approaches you can use with heap memory:
- Nested containers (e.g. vector<vector<...>>), and
- One container using arithmetic to produce a 2D "projection"
1. Nested containers
A container of a container like a std::vector<std::vector<T>> is likely the easiest way. This is the recommended approach over manually-managed heap-allocated pointers (e.g. don't use new T*[N] followed by a bunch of new T[M] pointers. See 'Why should C++ programmers minimize use of 'new'?' for more details).
This can be done easily:
auto rows    = std::size_t{};
auto columns = std::size_t{};
// Get the input (ignoring prompts for the sake of brevity)
std::cin >> rows;
std::cin >> columns;
// using 'T' as a placeholder for the type
auto array_2d = std::vector<std::vector<T>>{}; 
array_2d.reserve(rows);
// Create 'row' number of vector objects
for (auto i = 0u; i < rows; ++i) {
    array_2d.push_back();     // create a new vector
    array_2d.resize(columns); // resize the vector to the number of columns
}
Note that this does not create a true "2d array" -- but rather it creates a container that holds row number of containers, each which is a container holding column T objects.
2. A single container, with a projection
The second way is to use a single container, such as a std::vector<T>, but to write a wrapper that projects a 2D array over it. For example, you could have a get(row, column) function that will access the element in the single contiguous vector and return it. This creates only 1 contiguous chunk of objects for the vector, but it's also (slightly) more complicated.
class Array2D {
public:
    Array2D(std::size_t rows, std::size_t columns)
        : m_data{},
          m_rows{rows},
          m_columns{columns}
    {
        m_data.resize(rows * columns);
    }
    auto get(std::size_t row, std::size_t column) -> T& {
        // you could also do checking here
        return m_data[row * m_columns + column];
    }
    auto get(std::size_t row, std::size_t column) const -> const T&; // can do the same for a const-qualified one...
    // Note: if you are using C++23, you can also have operator[] with
    //       more than one argument
    // ...
private:
    std::vector<T> m_data;
    std::size_t m_rows;
    std::size_t m_columns;
};
This would then be used like:
auto rows    = std::size_t{};
auto columns = std::size_t{};
// Get the input (ignoring prompts for the sake of brevity)
std::cin >> rows;
std::cin >> columns;
auto array = Array2D{rows, columns};
// Get a reference to a value
auto& v = array.get(0,5);
// Set a value
array.get(0,5) = ...
if you want to keep an array[row][column] syntax, you could also implement operator[] to return a proxy object so that you could make the syntax behave more like a 2D array:
class Array2DProxy {
public:
    explicit Array2DProxy(T* row) : m_row{row}{}
    auto operator[](std::size_t column) -> T& { 
        return m_row[column];
    }
private:
    T* m_row;
};
class Array2D {
    ...
    auto operator[](std::size_t row) -> Array2DProxy {
        // Return a proxy object using a pointer to the start of the row
        return Array2DProxy{&m_data[row * m_columns]};
    } 
    ...
}