Merge Sort C++11 C++17
Design
Passing data with high and low works. But is equivalent to data+low and data+high. And just passing two values.
This is what is referred to as an Iterator range. If you look at the standard library you will see that a lot of the concepts hinge around iterators you should look that up and understand how it works.
Code Review
For every call to new there should be a call to delete.
    int* sorted = new int[size];

Or so the old text books tell us. In modern C++ you probably should never call new or delete (unless you know its a good idea).
But here you are leaking memory because you don't release it. You should add a delete [] sorted; to the end of the function.
BUT leaving the new/delete in your code is not a good idea. It's not exception safe to start with. So simply declare a vector&lt;int&gt; here and let it manage the memory for you.
We should also note that memory management is very expensive. So rather than doing this every loop. Why not do it once at the beginning and reuse the memory.
Sure this works:
    while(low_copy &lt; mid &amp;&amp; mid_copy &lt; high) {
        if(data[low_copy] &lt; data[mid_copy]) {
            sorted[i] = data[low_copy++];
        }
        else {
            sorted[i] = data[mid_copy++];
        }
        ++i;
    }

But we could simplify it:
     for(;low_copy &lt; mid &amp;&amp; mid_copy &lt; high; ++i) {
        sorted[i] = (data[low_copy] &lt; data[mid_copy])
                       ? data[low_copy++];
                       : data[mid_copy++];
    }

Also when you see loops like this you should look at the standard algorithms to see if there is one that will do all that work for you.
Another thing to note is that you are copying the value from data to sorted. This is fine for integers, but you should be able to sort nearly any C++ type and copying a C++ type is not usually the most efficient way to get the value from one location to another. You should look up the concept of moving. When the type being sorted is a bit more complex moving it (rather than copying is usually a better idea).
These loops can definitely be done by standard algorthims:
    while(low_copy &lt; mid) {
        sorted[i++] = data[low_copy++];
    }
    // Or 
    std::copy(data + low_copy, data + mid, sorted + i);
    // Or (when you move objects)
    std::move(data + low_copy, data + mid, sorted + i);

This is subject to overflow.
    int mid = (high + low) / 2;

Common mistake. If high and low are both large then this could easily overflow. Do the division fist (on the difference). Then add to the low.
    int mid = low + (high - low) / 2;

Are you sure there are 9 elements?
    int data[] = { 5, 1, 4, 3, 65, 6, 128, 9, 0 };
    int num_elements = 9;

Common source of errors. Don't manually do stuff the compiler can work out correctly. This is especially true if data is subsequently modified (and human person can make mistakes) does not change num_elements.
    int data[] = { 5, 1, 4, 3, 65, 6, 128, 9, 0 };
    int num_elements = sizeof(data)/sizeof(data[0]);

In more modern versions of C++ we even have a standard way of doing it:
    int num_elements = std::size(data);

No need for this in main:
    return 0;