Linked List Interview Code methods, runtime, and edge cases refactored
I see a number of things that could help you improve your code.

Don't define a default constructor that only initializes data

Instead of writing this:

class LinkedList
{
    Node* _head;
    Node* _tail;
 public:
    LinkedList() : _head(nullptr) { }
    // etc.
};


write this:

class LinkedList
{
    Node* _head = nullptr;
    Node* _tail = nullptr;
 public:
    // no need to explicitly write default constructor
    LinkedList() = default;
};


See Cpp Core Guidelines C.45 for details.  Note also that this initializes both member data items.

Think about whether the destructor should be virtual

For an interview, you should be prepared to answer why the class destructor is not virtual and to explain circumstances in which it should be virtual.

Eliminate redundant variables

The current code contains this constructor:

LinkedList(int data) { // O(1)
    Node* n = new Node(data);
    _head = n;
    _tail = n;
}


I'd prefer to see n eliminated and to see it written instead like this:

LinkedList(int data) :
    _head{new Node{data}}, _tail{_head} {}


Use for rather than while where appropriate

The desructor currently looks like this:

~LinkedList() { // O(n)
    Node* current = _head;
    Node* n;
    while(current) {
        n = current;
        current = current-&gt;next;
        delete n;
    }
    _head = nullptr;
}


Rather than introducing two extra variables, I'd write it using a for loop like this:

~LinkedList() { // O(n)
    if (_head == nullptr) {
        return;
    }
    for (auto current{_head-&gt;next}; current; current = current-&gt;next) {
        delete _head;
        _head = current;
    }
}


Fix the memory leak

The current code has this member function:

void append(int new_data) { // O(1)
    auto n = new Node(new_data);
    if(!_head) {
       _head = n;
       _tail = n;
    }
    else {
        _tail-&gt;next = new Node(new_data);
        _tail = _tail-&gt;next;
    }
}


The problem is that we create two nodes but only use one unless the list is empty.  I'd fix that like this:

void append(int new_data) { // O(1)
    auto n{new Node(new_data)};
    if(_head == nullptr) {
       _head = _tail = n;
    } else {
        _tail-&gt;next = n;
        _tail = _tail-&gt;next;
    }
}


Simplify your code

The remove function is much more longer than it needs to be.  Right now it looks like this:

    void remove(int target_data) { // O(n)
        if(!_head) { return; }
        Node* previous = nullptr;
        auto current = _head;
        for(; current-&gt;data != target_data; current = current-&gt;next) {
            if(!current-&gt;next) {
                return;
            }
            previous = current;
        }
        if(!current-&gt;next) {
            if(previous) {
                delete current;
                previous-&gt;next = nullptr;
            }
            else {
                 delete current;
                _head = nullptr;
            }
        }
        else {
            auto dangling_ptr = current-&gt;next;
            current-&gt;data = current-&gt;next-&gt;data;
            current-&gt;next = current-&gt;next-&gt;next;
            delete dangling_ptr;
        }
    }


It could be made somewhat simpler by creating a temporary Node that points to _head:

void remove(int target_data) { // O(n)
    Node headptr{0};
    headptr.next = _head;
    for (Node* curr{&amp;headptr}; curr &amp;&amp; curr-&gt;next; curr = curr-&gt;next) {
        if (curr-&gt;next-&gt;data == target_data) {
            auto victim = curr-&gt;next;
            if (victim == _tail) {
                _tail = curr;
            }
            if (victim == _head) {
                _head = victim-&gt;next;
            }
            curr-&gt;next = victim-&gt;next;
            delete victim;
            return;
        }
    }
}


Rethink the interface

It's not unreasonable to expect that one might want to actually do something with the things inserted into a list.  Right now, there's no way to do anything except print which suggests an incomplete interface.

Don't use std::endl if '\n' will do

Using std::endl emits a \n and flushes the stream.  Unless you really need the stream flushed, you can improve the performance of the code by simply emitting '\n' instead of using the potentially more computationally costly std::endl.

Make test success obvious

The current test code exersizes the list, but it doesn't indicate what is expected to be printed.  I'd instead write both test scenarios and also the expected result so that it would be clear to anyone running the code whether everything was working as expected or not.

Consider using a template

A linked list is a fairly generic structure that could hold any kind of data if the class were templated, and not just an int.

Consider possible uses

For any code production, but especially if you're in an interview, think about how the class is being used and whether there are any restrictions or limits inherent in the design.  For example, think about copy and move operations.  If you write this, does the code do the right thing?

LinkedList a;
a.append(1);
a.append(2);
a.append(3);
a.append(4);
auto b{a};
std::cout &lt;&lt; "a: " &lt;&lt; a &lt;&lt; '\n';
std::cout &lt;&lt; "b: " &lt;&lt; b &lt;&lt; '\n';
a.remove(1);
std::cout &lt;&lt; "a: " &lt;&lt; a &lt;&lt; '\n';
std::cout &lt;&lt; "b: " &lt;&lt; b &lt;&lt; '\n';


Also consider multithreaded code.  Would it be thread-safe to insert nodes from one thread and remove them from another?  If not, what would be needed to make that work?